using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Collections;
using System.Text.RegularExpressions;

namespace ConsoleApplication1
{
    class Program
    {
        //all the lines read in from the file
        static string[] lines;
        //the current line
        static string line;
        //output filename, where the machine code will be written to - default is output.bin
        static string outputFilename = @"output.bin";
        //input filename, where mnemonics are read from
        static string inputFilename;
        //hashtable of label names
        static Hashtable labels;
        //List of all the errors encountered
        static List<string> errorList;
        //list of all the completely parsed machine codes
        static List<string> outputCode;
        //hashTable of all currently defined constants
        static Hashtable constants;
        //enumeration of all the operand types - register, immediate, and memory
        enum opTypes
        {
            reg, immed, mem
        }
        //the current line the assembler is on
        static int currLine;
        //the current address of the instruction - incremented by 1 whenever an instruction is parsed
        static int currAddress;

        //immediate opcode format
        static List<string> immediateInstrs;

        //opcode only format
        static List<string> opcodeOnlyInstrs;

        //3 operand opcode instructions
        static List<string> _3operandInstrs;

        //shift and rotate instruction format
        static List<string> shiftAndRotateInstrs;

        static void Main(string[] args)
        {

            //makes sure input arguments are formatted correctly
            if (args.Length == 0)
            {
                Console.WriteLine("Usage: 554asm inputfile <outputfile>");
            }
            if (args.Length == 2)
            {
                outputFilename = args[1];
            }
            else if (args.Length != 1) //throw an error - no arguments or too many arguments specified
            {
                throw new ArgumentException("Error - too many arguments. type '554asm' for correct synatx");
            }


            //read the input file
            inputFilename = args[0];
            try
            {
                ArrayList arrList = new ArrayList();
                StreamReader input = new StreamReader(inputFilename);
                while (!input.EndOfStream)
                {
                    arrList.Add(input.ReadLine());
                }


                //assign each line to a spot in the arrayLists
                lines = (String[])arrList.ToArray(typeof(string));
            }
            //handle any errors reading the file
            catch (Exception e)
            {
                Console.WriteLine("Could not open " + inputFilename + " for reading" + e.ToString());
                throw new IOException("exception opening file: " + inputFilename);
                return;
            }

            //at this point, all the lines have been read into memory
            //begin parsing each line using the appropriate delegate

            //clear errorlist
            errorList = new List<string>();
            //clear the outputcode list
            outputCode = new List<string>();
            //set the current address to 0
            currAddress = 0;
            //setup the labels hashTable
            labels = new Hashtable();
            //setup the constants hashtable
            constants = new Hashtable();

            //setup all of the mnemonic lists
            immediateInstrs = new List<string>();
            opcodeOnlyInstrs = new List<string>();
            _3operandInstrs = new List<string>();
            shiftAndRotateInstrs = new List<string>();
            initList(opcodeOnlyInstrs, new string[] { "hlt", "nop" });
            initList(immediateInstrs, new string[] {"addi", "subi", "muli", "bgt", "blt", "bgte", "blte", 
                                                    "beq", "bne", "lui", "not", "andi", "ori", "xori",
                                                    "bneq", "jal", "jr", "sw", "store", "lw", "load"});
            initList(_3operandInstrs, new string[] { "mov", "add", "sub", "mul", "and", "or", "xor", "shrlv", "shrav", "shlv", "rorv", "rolv" });
            initList(shiftAndRotateInstrs, new string[] { "shrl", "shra", "shl", "ror", "rol" });

            //process all the defines & labels first, so we can know immediately if the mnemonics are valid or not
            for (int i = 0; i < lines.Length; i++)
            {
                line = lines[i].Trim();
                currLine = i;
                string[] fields = line.Split(new char[] { ' ', ',' });
                if (isValidMnemonic(fields[0]))
                {
                    currAddress++;
                }
                //if this is a comment
                if (line.Contains("#def"))
                {
                    string[] defineStatement = line.Split(new string[] { "#def", " " }, StringSplitOptions.RemoveEmptyEntries);
                    if (defineStatement.Length == 2)
                    {
                        Console.WriteLine("adding constant: {0}, {1}", defineStatement[0], defineStatement[1]);
                        try
                        {
                            constants.Add(defineStatement[0], Int16.Parse(defineStatement[1]));
                        }
                        catch (Exception e)
                        {
                            errorList.Add("Error parsing constant: " + line + "\n on line " + i + "\n\n");
                        }
                    }
                }
                //go through and add each label before we do any offset calculations or anything like that.
                else if(line.EndsWith(":"))
                {
                    string label = line.Split(':')[0];
                    labels.Add(label, currAddress);
                }
            }

            for (int i = 0; i < lines.Length; i++)
            {
                //i is the current value of the line
                line = lines[i].Trim();

                if (line.Length >= 2)
                {
                    if (line.Substring(0, 2).Equals("//") || line.Substring(0, 2).Equals("/*") || line.StartsWith("#"))
                    {
                        //we have a comment, so do nothing
                    }
                    else if (line.EndsWith(":"))  //if the line is a label, add it to the label handler
                    {
                       //It's a label. Ignore it! 
                    }
                    else  //valid opcode, so commence the parsing!
                    {
                        //wrap this in a try catch block in case the method spits some kind of nasty error
                        try
                        {
                            string machineCode = parseMnemonic(line);
                            outputCode.Add(machineCode);
                            //increment the current address the assembler is at
                            currAddress++;
                        }
                        catch (Exception e)
                        {
                            //the parsing method barfed on parsing the mnemonic,
                            //print out the line number where the error occurred, along with the error
                            errorList.Add("Error on line " + i + ": " + line + "\n" + line + "\n" + e.ToString() + "\n\n");
                        }
                    }
                }
            }

            //at this point, we're done trying to parse each line.
            //however, the operation may or may not have completed successfully.
            //if there are no errors, then go ahead and write the file, else output the errors.


            if (errorList.Count == 0)
            {
                //if there are no errors, write the output file

                Console.WriteLine("\nFile assembled successfully\n");
                writeLoadFile(outputCode);
            }
            else
            {
                dumpLabels();
                //print out all the errors in the errorlist.
                foreach (string currentErr in errorList)
                {
                    Console.Write(currentErr);
                    //write that errors occurred
                    Console.WriteLine("{0} Errors found. Output file not written", errorList.Count);
                }
            }
        }

        static string parseMnemonic(string line)
        {
            Console.WriteLine("parsemenmonic: " + line);
            //trim the line
            line = line.Trim();
            //try splitting the line
            string[] fields = line.Split(new Char[] { ' ', ',' }, StringSplitOptions.RemoveEmptyEntries);
            return parseOpcode(fields);
        }

        static string catNum(char c, int num)
        {
            string toReturn = "";
            for (int i = 0; i < num; i++)
            {
                toReturn += c;
            }
            return toReturn;
        }


        /// <summary>
        ///  Parses each opCode. Blank lines, comments, and labels should not be passed to this method.
        /// Guarantees: no invalid operands are passed to the instruction, no extra operands, etc. etc.
        /// </summary>
        /// <param name="fields">An array of strings representing the different operands in the instruction</param>
        /// <returns>string representation of the binary value of this </returns>
        static string parseOpcode(string[] fields)
        {
            //the binary representation of the instruction
            string binary = "";

            //strings used for quicker comparisons & concatenations
            string _5z = "00000";
            string _6z = "000000";
            string _8z = "00000000";
            string _51s = "11111";
            string _61s = "111111";
            string nopCode = "00000000000000000000000000000000";
            string haltCode = "11111111111111111111111111111111";
            string opcode = fields[0].ToLower();
            string fieldA, fieldB, fieldC;


            //assign each field to a more friendly fieldA, fieldB, fieldC format.
            try
            {
                fieldA = fields.Length > 1 ? fields[1] : null;
                fieldB = fields.Length > 2 ? fields[2] : null;
                fieldC = fields.Length > 3 ? fields[3] : null;
            }
            catch (Exception e)
            {
                string errorMsg = "";
                foreach (string s in fields)
                {
                    errorMsg += s + " ";
                }
                throw new Exception("Could not fields out: " + fields);
            }

            //if there are too many operands, e.g. add $1, $2, $3, $4
            if (fields.Length > 4)
                throw new Exception("Too many operands in instruction");

            //figure out which list the current opcode is in
            if (opcodeOnlyInstrs.Contains(opcode))
            {
                //if an exception has not been thrown, then return the binary representation
                if (opcode.Equals("nop"))
                    return nopCode;   //return 32 zeroes
                else
                    return haltCode;

            }
            else if (_3operandInstrs.Contains(opcode))
            {
                //verify there are 3 operands
                Hashtable instrCodes = new Hashtable();
                instrCodes.Add("add", "000001");
                instrCodes.Add("sub", "000011");
                instrCodes.Add("mul", "000101");
                instrCodes.Add("and", "001001");
                instrCodes.Add("or",  "001011");
                instrCodes.Add("xor", "001101");
                instrCodes.Add("shrlv", "001111");
                instrCodes.Add("shrav", "010001");
                instrCodes.Add("shlv", "010011");
                instrCodes.Add("rorv", "010101");
                instrCodes.Add("rolv", "010111");
                if(instrCodes.Contains(opcode))
                {
                    int[] types = new int[] { (int) opTypes.reg, (int) opTypes.reg, (int) opTypes.reg};
                    string[] operands = new string[] {fieldA, fieldB, fieldC};
                    types = checkOperandTypes(operands, types);
                    return instrCodes[opcode] + catNum('0', 11) + binaryRegNum(types[0]) + binaryRegNum(types[1]) + binaryRegNum(types[2]); 
                }
                //handle the weird 2 operand  NOT instruction
            }
            //immediate instruction format
            else if (immediateInstrs.Contains(opcode))
            {
                //handle all the branch instructions
                if (opcode.Equals("beq") || opcode.Equals("bne") || opcode.Equals("bgt") || opcode.Equals("bgte") || opcode.Equals("blt")
                    || opcode.Equals("blte") || opcode.Equals("bneq"))
                {
                    int[] types = new int[] { (int) opTypes.reg, (int) opTypes.reg, (int) opTypes.immed };
                    string[] operands = new string[] { fieldA, fieldB, fieldC };
                    types = checkOperandTypes(operands, types);
                    Hashtable opcodesBin = new Hashtable();
                    opcodesBin.Add("beq", "100000");
                    opcodesBin.Add("bne", "100001");
                    opcodesBin.Add("bneq", "100001");
                    opcodesBin.Add("bgt", "100010");
                    opcodesBin.Add("bgte", "100011");
                    opcodesBin.Add("blt", "100100");
                    opcodesBin.Add("blte", "100101");
                    
                    return opcodesBin[opcode] + convertToBin((short) types[2]) + binaryRegNum(types[0]) + binaryRegNum(types[1]);
                }
                Hashtable instrCodes = new Hashtable();
                instrCodes.Add("addi", "000010");
                instrCodes.Add("subi", "000100");
                instrCodes.Add("andi", "001010");
                instrCodes.Add("ori",  "001100");
                instrCodes.Add("xori", "001110");
                //arithmetic & bitwise immediate instructions
                if(instrCodes.Contains(opcode))
                {
                    int[] types = new int[] {(int) opTypes.immed, (int) opTypes.reg, (int)opTypes.reg};
                    string[] operands = new string[] {fieldA, fieldB, fieldC};
                    types = checkOperandTypes(operands, types);
                    return instrCodes[opcode] + convertToBin((short)types[0]) + binaryRegNum(types[1]) + binaryRegNum(types[2]);
                }
                //handle the jal instruction
                else if (opcode.Equals("jal"))
                {
                    //come back to this instruction because it is in the 'memory' format
                    int[] types = new int[] { (int) opTypes.mem};
                    string[] operands = new string[] {fieldA};  //
                    types = checkOperandTypes(operands, types);
                    return "100110" + convertToBin((short)types[0]) + binaryRegNum(types[1]) + _51s;
                }

                //handle the jr instruction
                else if (opcode.Equals("jr"))
                {
                    //should only have 1 operand, a register
                    short regNum = check1OperandType(fieldA, (int)opTypes.reg);
                    return "100111" + catNum('0', 16) + binaryRegNum(regNum) + catNum('0', 5);
                }
                else if (opcode.Equals("not"))
                {
                    int[] types = new int[] { (int)opTypes.reg, (int)opTypes.reg };
                    string[] operands = new string[] { fieldA, fieldB };
                    types = checkOperandTypes(operands, types);
                    return "001000" + catNum('0', 16) + binaryRegNum(types[0]) + binaryRegNum(types[1]);
                }
                //handle the lui instruction
                else if (opcode.Equals("lui"))
                {
                    int[] types = new int[] { (int)opTypes.immed, (int)opTypes.reg };
                    string[] operands = new string[] { fieldA, fieldB };
                    types = checkOperandTypes(operands, types);
                    return "000110" + convertToBin((short)types[0]) + binaryRegNum(types[1]) + _5z;
                }
                else if (opcode.Equals("lw") || opcode.Equals("load"))
                {
                    int[] types = new int[] { (int)opTypes.reg, (int)opTypes.mem };
                    string[] operands = new string[] { fieldA, fieldB };
                    types = checkOperandTypes(operands, types);
                    return "110000" + convertToBin((short)types[1]) + binaryRegNum(types[2]) + binaryRegNum(types[0]);
                }
                else if (opcode.Equals("sw") || opcode.Equals("store"))
                {
                    int[] types = new int[] { (int)opTypes.reg, (int)opTypes.mem };
                    string[] operands = new string[] { fieldA, fieldB };
                    types = checkOperandTypes(operands, types);
                    return "110001" + convertToBin((short)types[1]) + binaryRegNum(types[2]) + binaryRegNum(types[0]);
                }
            }
            else if(shiftAndRotateInstrs.Contains(opcode))
            {
                Hashtable instrCodes = new Hashtable();
                instrCodes.Add("shrl", "010000");
                instrCodes.Add("shra", "010010");
                instrCodes.Add("shl",  "010100");
                instrCodes.Add("ror",  "010110");
                instrCodes.Add("rol",  "011000");

                int[] types = new int[] { (int) opTypes.reg, (int)opTypes.immed, (int)opTypes.reg};
                string[] operands = new string[] {fieldA, fieldB, fieldC};
                types = checkOperandTypes(operands, types);
                return instrCodes[opcode] + catNum('0', 11) + binaryRegNum(types[0]) + binaryRegNum(types[1]) + binaryRegNum(types[2]);

            }
            else //if the opcode is not in any lists, then throw an error, add to error list
            {
                throw new Exception("Invalid opcode: " + opcode);
            }

            //return the binary representation of the command
            return binary;
        }

        static void writeLoadFile(List<string> lines)
        {
            try   //open a new stream for writing to the output file.
            {
                //this will overwrite anything in the file previously.
                StreamWriter sw = new StreamWriter(outputFilename);
                foreach (string machineCode in outputCode)
                {
                    sw.WriteLine(machineCode);
                }
                //close the file
                sw.Close();
            }
            catch (Exception e) //Handle a file I/O exception
            {
                throw new Exception("Error writing " + outputFilename);
            }
        }

        static List<string> initList(List<string> l, string[] arr)
        {
            foreach (string s in arr)
                l.Add(s);
            return l;
        }

        /// <summary>
        /// checks to see if the number of operands in the instruction is correct
        /// </summary>
        /// <param name="fields">the list of opcode & operands</param>
        /// <param name="numOps">the number of operands required</param>
        static void checkNumOperands(string[] fields, int numOps)
        {
            /* Debug */
            /*
            foreach (string s in fields)
            {
                Console.WriteLine("Field: {0}\n", s);
            }
            */
            if (fields.Length != numOps)
                throw new Exception("incorrect number of operands in instruction.");
        }


        /// <summary>
        /// Checks 1 operand types to make sure it is consistent with our types
        /// </summary>
        /// <param name="field">the operand</param>
        /// <param name="i"> the type the operand should be</param>
        /// <returns></returns>
        static short check1OperandType(string field, int i)
        {
            /*Debug*/
            /*  Console.WriteLine("field is: " + field); /**/

            if (registerNumber(field) != -1 && i == (int)opTypes.reg)    //if it's a register, call registerNumber
                return (short)registerNumber(field);
            else if (i == (int)opTypes.immed)
            {
                //Console.WriteLine("immediate type check");
                if (constants.Contains(field))  //see if it's a constant in the constants table
                {
                    return Int16.Parse(constants[field].ToString());
                }
                else if (labels.Contains(field))
                {
                    Console.WriteLine("Label {0} found - addr: {1}", field, labels[field]);
                    return Int16.Parse(labels[field].ToString());
                }
                try
                {
                    return Int16.Parse(field);
                }
                catch (Exception e)
                {
                    throw new Exception("invalid operand " + field);
                }
            }
            throw new Exception("Field: " + field + " is in an unrecognized format");
        }

        /// <summary>
        /// checks each operand to make sure it is the correct type and then returns
        /// the parsed value (in decimal) for each operand.
        /// </summary>
        /// <param name="fields">the array of fields (including mnemonic) to be examined</param>
        /// <param name="opTypes"></param>
        /// <returns></returns>
        static int[] checkOperandTypes(string[] fields, int[] operandTypes)
        {
            checkNumOperands(fields, operandTypes.Length);
            //loop through every operand and make sure it checks out as the correct opcode type.
            //if it doesn't throw an exception, then we put the value of the operand back into the operandTypes register
            //this is being a little bit tricky
            for(int i = 0; i < operandTypes.Length; i++)
            {
                if (operandTypes[i] == (int)opTypes.mem)
                {
                    //the basic gist is that we need to add another operand type to be validated
                    //so create a new operandTypes array, copy all the old values, add the new values
                    int[] newOperandTypes = new int[operandTypes.Length + 1];
                    for (int j = 0; j < newOperandTypes.Length; j++)
                    {
                        if (j == i)
                        {
                            newOperandTypes[i] = (int)opTypes.immed;
                            j++;
                            newOperandTypes[j] = (int)opTypes.reg;
                            continue;
                        }
                        else  //otherwise, copy the old values *FUCK*
                        {
                            newOperandTypes[j] = operandTypes[j];
                        }
                    }
                    //create a new array for the operands themselves

                    //if there are parenthesis, then two operands
                    Match m = Regex.Match(fields[0], @"(^\d{1,5})\((\$\d{0,2}|sp)\)");
                    operandTypes = new int[] { (int)opTypes.immed, (int)opTypes.reg };
                    if (m.Success)
                    {

                        Console.WriteLine("total # of groups: {0}", m.Groups.Count.ToString());
                        foreach (Group c in m.Groups)
                        {
                            Console.WriteLine("Group: {0}", c.ToString());
                        }
                        fields = new string[] { m.Groups[1].ToString(), m.Groups[2].ToString() };
                    }
                    //else, figure out if register or immediate
                    //substitute in either an immediate 0 or $0
                    else if (fields[0][0] == '$') //register only format, so set the immediate portion to "0"
                    {
                        fields = new string[] { "0", fields[0] };
                        Console.WriteLine("reg. only mem. format. Sending: {0}, {1}", fields[0], fields[1]);
                    }
                    else    //immediate specified, so make the register = "$0" 
                    {

                        fields = new string[] { fields[0], "$0" };
                        Console.WriteLine("immediate only mem. format. Sending: {0}, {1}", fields[0], fields[1]);
                    }
                    //call check1OperandType on newly created array
                }
            }
            for (int i = 0; i < operandTypes.Length; i++)
            {
                string field = fields[i];
                //check the appropriate operand with its required type
                operandTypes[i] = check1OperandType(field, operandTypes[i]);
            }
            return operandTypes;
        }

        /// <summary>
        /// returns the number of a register given a string in the format $16 or $sp, etc.
        /// </summary>
        /// <param name="operand">string register specifier</param>
        /// <returns>.</returns>

        static int registerNumber(string operand)
        {
            string[] regParts = operand.Split('$');
            //DEBUG
            /*
            if (regParts.Length == 2)
            {
                Console.WriteLine("Register designator: {0}. reg number {1}", regParts[0], regParts[1]);
            }
            /* */
            if (regParts.Length == 2 && operand[0] == '$')
                try
                {
                    if (Int16.Parse(regParts[1]) >= 0 && Int16.Parse(regParts[1]) <= 31)    //make sure the register is between 0 and 31 inclusive
                        return Int32.Parse(regParts[1]);
                }
                catch (Exception e)
                {
                    if (regParts[1] == "sp")    //if the stack pointer register has been specified
                        return 31;
                }
            //invalid register specifier
            return -1;
        }

        static string binaryRegNum(int regnum)
        {
            switch (regnum)
            {
                case(0):  return "00000";
                case (1): return "00001";
                case (2): return "00010";
                case (3): return "00011";
                case (4): return "00100";
                case (5): return "00101";
                case (6): return "00110";
                case (7): return "00111";
                case (8): return "01000";
                case (9): return "01001";
                case (10): return "01010";
                case (11): return "01011";
                case (12): return "01100";
                case (13): return "01101";
                case (14): return "01110";
                case (15): return "01111";
                case (16): return "10000";
                case (17): return "10001";
                case (18): return "10010";
                case (19): return "10011";
                case (20): return "10100";
                case (21): return "10101";
                case (22): return "10110";
                case (23): return "10111";
                case (24): return "11000";
                case (25): return "11001";
                case (26): return "11010";
                case (27): return "11011";
                case (28): return "11100";
                case (29): return "11101";
                case (30): return "11110";
                case (31): return "11111";
                default: return "";
            }
        }

        /// <summary>
        /// Adds a new label to the hashtable of labels
        /// </summary>
        /// <param name="line">the line with the label name on it</param>
        /// <returns>the address the label is mapped to</returns>
        static int addLabel(string line)
        {
            line = line.Trim();
            try
            {
                string lblName = line.Split(':')[0];
                //check for duplicate labels
                if (labels.Contains(lblName))
                {
                    errorList.Add("Error: " + lblName + " already exists at offset " + labels[lblName] + "\n" );
                }
            }
            catch (Exception e)
            {
                errorList.Add("Error on line: " + currLine + " - could not parse label name " + line + "\n" + e.ToString() + "\n");
            }
            return currAddress;
        }

        static int calcOffset(string lblName)
        {
            try
            {
                return (int) labels[lblName] - currAddress;
            }
            catch (Exception e)
            {
                errorList.Add("Error on line: " + currLine + " - could nto calculate offset to label: " + lblName + "\n" + e.ToString() + "\n");
            }
            return 0;
        }

        static bool isValidMnemonic(string mnemonicName)
        {

            return (opcodeOnlyInstrs.Contains(mnemonicName) || _3operandInstrs.Contains(mnemonicName)
                || immediateInstrs.Contains(mnemonicName) || shiftAndRotateInstrs.Contains(mnemonicName));
        }

        static string convertToBin(Int16 decimalNum)
        {

            string bin = Convert.ToString(decimalNum, 2);
            bin = catNum('0', 16 - bin.Length) + bin;
            //Debug
            //Console.WriteLine("binary representation of decimalNum {0} : {1}", decimalNum, bin);
            return bin;
        }

        static void dumpLabels()
        {
            Console.WriteLine("Dumping labels table");
            foreach (string label in labels.Keys)
            {
                Console.WriteLine("Label name: {0} - addr: {1}", label, labels[label]);
            }
            Console.WriteLine();
        }
    }
}
